﻿using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Dynamic;
using System.Linq.Expressions;
using System.Reflection;
using Telerik.Web.Mvc;
using Telerik.Web.Mvc.Extensions;
using Telerik.Web.Mvc.Infrastructure;
using TelerikMvcGridCustomBindingHelper.Aggregates;
using TelerikMvcGridCustomBindingHelper.Exceptions;
using TelerikMvcGridCustomBindingHelper.Mapper;
using TelerikMvcGridCustomBindingHelper.Projections;
using TelerikMvcGridCustomBindingHelper.Util;

namespace TelerikMvcGridCustomBindingHelper
{
    /// <summary>
    /// Base class to make Helper classes to work with Telerik Grid for ASP.NET MVC using CustomBinding mode coupled with
    /// some mapping techniques to convert Entities objects into ViewModels objects.
    /// </summary>
    /// <typeparam name="TEntity">The type of your Entity class</typeparam>
    /// <typeparam name="TViewModel">The type of your ViewModel class</typeparam>
    public abstract class BaseGridCustomBindingHelper<TEntity, TViewModel>
        where TViewModel : class
        where TEntity : class
    {
        #region Properties

        /// <summary>
        /// Default sort member name.
        /// </summary>
        public string DefaultSortMemberName { get; set; }

        /// <summary>
        /// List of viewmodels, may be a set of groups and aggregates or a simple list.
        /// </summary>
        public IEnumerable ProcessedData
        {
            get
            {
                EnsureDataSourceIsProcessed();
                return ProcessedDataField;
            }
        }

        /// <summary>
        /// IEnumerable of viewmodels without groups or aggregates.
        /// </summary>
        public IEnumerable<TViewModel> ViewModels
        {
            get
            {
                EnsureDataSourceIsProcessed();
                return ViewModelsField;
            }
        }

        /// <summary>
        /// IEnumerable of entities from the database.
        /// </summary>
        public IEnumerable<TEntity> Entities
        {
            get
            {
                EnsureDataSourceIsProcessed();
                return EntitiesField;
            }
        }

        /// <summary>
        /// Total number of records in the database.
        /// </summary>
        public long Total
        {
            get
            {
                EnsureDataSourceIsProcessed();
                return TotalField;
            }
        }

        #endregion

        #region Fields

        protected ProjectionsOptionsImpl<TEntity> ProjectionsOptions;
        protected bool AcceptNullValuesWhenFilteringField;
        protected bool UseProjectionsField;
        protected bool IsDataSourceProcessed;
        protected long TotalField;
        protected IEnumerable ProcessedDataField;
        protected IEnumerable<TViewModel> ViewModelsField;
        protected IList<TEntity> EntitiesField;
        protected readonly GridCommand CommandField;
        protected Dictionary<string, object> AggregatesField;
        protected CustomAggregateFunctionBase InheritedClassCustomAggregateCalculatorField;
        protected bool IgnoreGroupAggregatesField;
        protected Func<IQueryable<TEntity>, object> ExpressionCustomAggregateFunctionField;
        private Func<IQueryable<TViewModel>, object> _aggregateViewModelFunctionField;
        internal CaseInsensitive<TEntity> CaseInsensitiveField;

        #endregion

        #region Constructors

        /// <summary>
        /// Initializes a new instance of the <see cref="BaseGridCustomBindingHelper{TEntity,TViewModel}"/> class. 
        /// Base constructor
        /// </summary>
        /// <param name="command">Telerik GridCommand containing informations about paging, filtering, sorting, aggregates</param>
        /// <param name="defaultSortMemberName">Default sort member name for this entity</param>
        protected BaseGridCustomBindingHelper(GridCommand command, string defaultSortMemberName)
        {
            DefaultSortMemberName = defaultSortMemberName;

            // Check if the command has been initialized
            CommandField = !IsEmptyCommand(command) ? command : new GridCommand();
        }

        #endregion

        #region Public Methods

        /// <summary>
        /// Create a new GridModel with the processed data and the total of records in the database
        /// </summary>
        /// <returns>GridModel</returns>
        public GridModel BuildGridModel()
        {
            EnsureDataSourceIsProcessed();

            return new GridModel { Data = ProcessedDataField, Total = (int)Total, Aggregates = AggregatesField };
        }

        #endregion

        #region Helper Functions

        protected abstract void EnsureDataSourceIsProcessed();

        private static bool IsEmptyCommand(GridCommand command)
        {
            return command.PageSize == 0;
        }

        protected static Expression<Func<TSource, TProperty>> GetPropertyExpression<TSource, TProperty>(string propName)
        {
            return Reflector.GetPropertyExpression<TSource, TProperty>(propName);
        }

        protected static GridPropertyInfo GetGridPropertyInfo(string viewModelPropertyName)
        {
            var gridPropertyInfo = GridModelMapper.GetPropertyMap<TEntity, TViewModel>(viewModelPropertyName);

            Guard.IfIsNull(gridPropertyInfo).Throw<PropertyNotFoundException>(typeof(TEntity), viewModelPropertyName);

            return gridPropertyInfo;
        }

        protected static IEnumerable<AggregateDescriptor> BuildGroupAggregateDescriptors(IEnumerable<GroupDescriptor> groupDescriptors)
        {
            var aggregateDescriptors = new List<AggregateDescriptor>();

            var aggregateDescriptor = new AggregateDescriptor();
            aggregateDescriptor.Aggregates.AddRange(groupDescriptors.First().AggregateFunctions);
            aggregateDescriptors.Add(aggregateDescriptor);

            return aggregateDescriptors;
        }

        protected static List<IFilterDescriptor> BuildGroupFilterDescriptors(IGroup @group, IEnumerable<GroupDescriptor> groupDescriptors, IList<IFilterDescriptor> filterDescriptors)
        {
            var filters = filterDescriptors.ToList();

            var groups = FlattenAggregateGroupsHierarchy(@group);

            filters.AddRange(groupDescriptors.Select(groupDescriptor =>
                new FilterDescriptor
                {
                    Member = groupDescriptor.Member,
                    Operator = FilterOperator.IsEqualTo,
                    Value = groups.Single(x => x.Member.Equals(groupDescriptor.Member)).Key
                }).ToList());

            return filters;
        }

        private static IEnumerable<CustomAggregateFunctionsGroup> FlattenAggregateGroupsHierarchy(IGroup firstGroup)
        {
            var aggregateFunctionsGroups = new List<CustomAggregateFunctionsGroup>();

            if (firstGroup.HasSubgroups)
            {
                foreach (var subgroup in firstGroup.Subgroups)
                {
                    aggregateFunctionsGroups.AddRange(FlattenAggregateGroupsHierarchy(subgroup));
                }
            }

            aggregateFunctionsGroups.Add((CustomAggregateFunctionsGroup)firstGroup);

            return aggregateFunctionsGroups.DistinctBy(x => x.Member).ToList();
        }

        #endregion

        #region ApplyDefaultSorting

        protected static void ApplyDefaultSorting(GridCommand command, string defaultSortMemberName)
        {
            if (command.SortDescriptors.Any() || command.GroupDescriptors.Any())
                return;

            ProcessDefaultSortMemberName(ref defaultSortMemberName, GridModelMapper.ConfigurationObject.DefaultSortMemberNameType);

            var defaultSortDirection = GridModelMapper.ConfigurationObject.DefaultSortDirection;

            if (string.IsNullOrWhiteSpace(defaultSortMemberName) == false)
            {
                command.SortDescriptors.Add(
                    new SortDescriptor { Member = defaultSortMemberName, SortDirection = defaultSortDirection });
            }
        }

        private static void ProcessDefaultSortMemberName(ref string defaultSortMemberName, IDMemberName idMemberName)
        {
            if (string.IsNullOrWhiteSpace(defaultSortMemberName))
            {
                var prefixOrSuffix = GridModelMapper.ConfigurationObject.DefaultSortMemberNameOrPrefixOrSuffix;
                switch (idMemberName)
                {
                    case IDMemberName.Id:
                        defaultSortMemberName = "Id";
                        break;
                    case IDMemberName.EntityNamePlusId:
                        defaultSortMemberName = string.Concat(typeof(TEntity).Name, "Id");
                        break;
                    case IDMemberName.IdPlusEntityName:
                        defaultSortMemberName = string.Concat("Id", typeof(TEntity).Name);
                        break;
                    case IDMemberName.PrefixPlusEntityName:
                        defaultSortMemberName = string.Concat(prefixOrSuffix, typeof(TEntity).Name);
                        break;
                    case IDMemberName.EntityNamePlusSuffix:
                        defaultSortMemberName = string.Concat(typeof(TEntity).Name, prefixOrSuffix);
                        break;
                    case IDMemberName.FixedMemberName:
                        defaultSortMemberName = GridModelMapper.ConfigurationObject.DefaultSortMemberNameOrPrefixOrSuffix;
                        break;
                    case IDMemberName.Guess:
                        defaultSortMemberName = GuessDefaultSortMemberName();
                        break;
                }

                if (string.IsNullOrWhiteSpace(defaultSortMemberName))
                    defaultSortMemberName = GridModelMapper.ConfigurationObject.DefaultSortMemberNameFunc(typeof(TEntity));
            }

            defaultSortMemberName = Reflector.FixMemberPathCase<TEntity>(defaultSortMemberName);
        }

        private static string GuessDefaultSortMemberName()
        {
            var tmp = string.Empty;

            ProcessDefaultSortMemberName(ref tmp, IDMemberName.Id);
            if (string.IsNullOrWhiteSpace(tmp) == false) return tmp;

            ProcessDefaultSortMemberName(ref tmp, IDMemberName.EntityNamePlusId);
            if (string.IsNullOrWhiteSpace(tmp) == false) return tmp;

            ProcessDefaultSortMemberName(ref tmp, IDMemberName.IdPlusEntityName);
            if (string.IsNullOrWhiteSpace(tmp) == false) return tmp;

            var properties = typeof(TEntity).GetProperties().Where(p => p.Name.StartsWith("Id") || p.Name.EndsWith("Id"));

            var firstOrDefault = properties.FirstOrDefault();

            return firstOrDefault != null ? firstOrDefault.Name : null;
        }

        #endregion

        #region ApplyGrouping

        protected IEnumerable ApplyGrouping(IEnumerable<TViewModel> data, IEnumerable<GroupDescriptor> groupDescriptors)
        {
            Func<IEnumerable<TViewModel>, IEnumerable<AggregateFunctionsGroup>> selector = null;

            foreach (var groupDescriptor in groupDescriptors.Reverse())
            {
                var propertyInfo = Reflector.GetMemberInfo<TViewModel>(groupDescriptor.Member).As<PropertyInfo>();

                Guard.IfIsNull(propertyInfo).Throw<PropertyNotFoundException>(typeof(TViewModel), groupDescriptor.Member);

                var addGroupExpressionMethod = Reflector.GetMemberName<BaseGridCustomBindingHelper<TEntity, TViewModel>>(x => x.AddGroupExpression<object>(null, null));

                var method = GetType().GetMethod(
                    addGroupExpressionMethod,
                    BindingFlags.Instance | BindingFlags.NonPublic,
                    Type.DefaultBinder,
                    new[] { typeof(GroupDescriptor), typeof(Func<IEnumerable<TViewModel>, IEnumerable<AggregateFunctionsGroup>>) },
                    null
                    );

                var genericMethod = method.MakeGenericMethod(new[] { propertyInfo.PropertyType });

                selector = genericMethod.Invoke(this, new object[] { groupDescriptor, selector }) as
                           Func<IEnumerable<TViewModel>, IEnumerable<AggregateFunctionsGroup>>;
            }

            if (selector != null)
                return selector.Invoke(data).ToList();

            return data;
        }

        // This method has to be protected so we can call it with reflection
        // ReSharper disable MemberCanBePrivate.Global
        protected Func<IEnumerable<TViewModel>, IEnumerable<AggregateFunctionsGroup>> AddGroupExpression<TProperty>(GroupDescriptor groupDescriptor, Func<IEnumerable<TViewModel>, IEnumerable<AggregateFunctionsGroup>> selector)
        {
            var exp = GetPropertyExpression<TViewModel, TProperty>(groupDescriptor.Member);
            return AddGroupExpression(exp.Compile(), selector, groupDescriptor);
        }
        // ReSharper restore MemberCanBePrivate.Global
        private Func<IEnumerable<TViewModel>, IEnumerable<AggregateFunctionsGroup>> AddGroupExpression<TKey>(Func<TViewModel, TKey> propertySelector, Func<IEnumerable<TViewModel>, IEnumerable<AggregateFunctionsGroup>> selector, GroupDescriptor groupDescriptor)
        {
            if (selector == null)
                return items => BuildInnerGroup(items, propertySelector, groupDescriptor);
            return BuildGroup(propertySelector, selector, groupDescriptor);
        }

        private Func<IEnumerable<TViewModel>, IEnumerable<AggregateFunctionsGroup>> BuildGroup<TKey>(Func<TViewModel, TKey> groupSelector, Func<IEnumerable<TViewModel>, IEnumerable<AggregateFunctionsGroup>> selectorBuilder, GroupDescriptor groupDescriptor)
        {
            var tempSelector = selectorBuilder;

            return group => group.GroupBy(groupSelector)
                .Select(g => new CustomAggregateFunctionsGroup
                                {
                                    Key = g.Key,
                                    HasSubgroups = true,
                                    Items = tempSelector.Invoke(g),
                                    Member = groupDescriptor.Member,
                                    Aggregates = ApplyGroupAggregates(g, groupDescriptor.AggregateFunctions, g.Key, groupDescriptor.Member)
                                });
        }

        private IEnumerable<AggregateFunctionsGroup> BuildInnerGroup<TKey>(IEnumerable<TViewModel> group, Func<TViewModel, TKey> groupSelector, GroupDescriptor groupDescriptor)
        {
            return group.GroupBy(groupSelector)
                .Select(g => new CustomAggregateFunctionsGroup
                                {
                                    Key = g.Key,
                                    Items = g.ToList(),
                                    Member = groupDescriptor.Member,
                                    Aggregates = ApplyGroupAggregates(g, groupDescriptor.AggregateFunctions, g.Key, groupDescriptor.Member)
                                });
        }

        private Dictionary<string, object> ApplyGroupAggregates(IEnumerable<TViewModel> enumerable, IEnumerable<AggregateFunction> aggregateFunctions, object groupKeyValue, string groupKeyName)
        {
            if (IgnoreGroupAggregatesField)
                return new Dictionary<string, object>();

            var aggregates = new Dictionary<string, AggregatesContainer>();
            enumerable = enumerable.ToList();

            foreach (var aggregateFunction in aggregateFunctions)
            {
                var viewModelProperty = aggregateFunction.SourceField;

                switch (aggregateFunction.AggregateMethodName)
                {
                    case "Sum":
                        aggregates.Push(viewModelProperty, enumerable.Sum(GetPropertyExpression<TViewModel, decimal?>(viewModelProperty).Compile()), AggregateMethod.Sum);
                        break;
                    case "Count":
                        aggregates.Push(viewModelProperty, enumerable.Count(), AggregateMethod.Count);
                        break;
                    case "Average":
                        aggregates.Push(viewModelProperty, enumerable.Average(GetPropertyExpression<TViewModel, decimal?>(viewModelProperty).Compile()), AggregateMethod.Average);
                        break;
                    case "Min":
                        aggregates.Push(viewModelProperty, enumerable.Min(GetPropertyExpression<TViewModel, decimal?>(viewModelProperty).Compile()), AggregateMethod.Min);
                        break;
                    case "Max":
                        aggregates.Push(viewModelProperty, enumerable.Max(GetPropertyExpression<TViewModel, decimal?>(viewModelProperty).Compile()), AggregateMethod.Max);
                        break;
                    case "Custom":
                        if (_aggregateViewModelFunctionField != null)
                            aggregates.Push(viewModelProperty, _aggregateViewModelFunctionField.Invoke(enumerable.AsQueryable()), AggregateMethod.Custom);
                        else if (ExpressionCustomAggregateFunctionField != null)
                        {
                            var entities = EntitiesField != null
                                ? EntitiesField.AsQueryable().Where(string.Format("{0} == @0", groupKeyName), groupKeyValue)
                                : GridModelMapper.Map<TViewModel, TEntity>(enumerable).AsQueryable();
                            aggregates.Push(viewModelProperty, SafeInvokeAggregateExpression(entities), AggregateMethod.Custom);
                        }
                        else if (InheritedClassCustomAggregateCalculatorField != null)
                            aggregates.Push(viewModelProperty, InheritedClassCustomAggregateCalculatorField.GetValueFromGroup(enumerable, EntitiesField, groupKeyValue, groupKeyName), AggregateMethod.Custom);
                        break;
                }
            }

            return aggregates.ToDictionary(x => x.Key, x => (object)x.Value.GetValues());
        }

        protected object SafeInvokeAggregateExpression(IQueryable<TEntity> entities)
        {
            try
            {
                return ExpressionCustomAggregateFunctionField.Invoke(entities);
            }
            catch
            {
                return string.Empty;
            }
        }

        public abstract class CustomAggregateFunctionBase
        {
            protected internal abstract object GetValueFromGroup(IEnumerable<TViewModel> viewModelsGroup, IList<TEntity> soruceEntities, object groupKeyValue, string groupKeyName);
        }

        #endregion

        #region Fluent API

        protected ValueResolvers QueryValueResolvers;

        protected BaseGridCustomBindingHelper<TEntity, TViewModel> AddValueResolver<TQueryValueResolver>(Expression<Func<TViewModel, object>> viewModelProperty) where TQueryValueResolver : class, IQueryValueResolver
        {
            Contract.Expect(() => viewModelProperty).IsNotNull();

            if (IsValidPropertyExpression(viewModelProperty) == false)
                throw new NotSupportedException(string.Format("This expression is not supported: {0}", viewModelProperty));

            var queryValueResolver = ObjectFactory<TQueryValueResolver>.Create();
            queryValueResolver.SetTargetProperty(viewModelProperty.GetPropertyPath());

            return AddValueResolver(queryValueResolver);
        }

        private static bool IsValidPropertyExpression(LambdaExpression expression)
        {
            var body = expression.Body;

            if (body.NodeType == ExpressionType.Convert)
            {
                body = ((UnaryExpression)body).Operand;
            }

            return body.NodeType == ExpressionType.MemberAccess;
        }

        protected BaseGridCustomBindingHelper<TEntity, TViewModel> AddValueResolver<TQueryValueResolver>() where TQueryValueResolver : class, IQueryValueResolver
        {
            return AddValueResolver(ObjectFactory<TQueryValueResolver>.Create());
        }

        protected BaseGridCustomBindingHelper<TEntity, TViewModel> AddValueResolver(IQueryValueResolver queryValueResolver)
        {
            QueryValueResolvers = QueryValueResolvers ?? new ValueResolvers();

            var valueResolverKey = ValueResolverKey.QueryKey(queryValueResolver);

            var containsKey = QueryValueResolvers.ContainsKey(valueResolverKey);

            while (containsKey)
            {
                IValueResolver vr;
                containsKey = QueryValueResolvers.TryRemove(valueResolverKey, out vr);
            }

            var tryAdd = QueryValueResolvers.TryAdd(valueResolverKey, queryValueResolver);

            Guard.If(tryAdd == false).Throw<ValueResolverRegistrationException>(queryValueResolver);

            return this;
        }

        protected BaseGridCustomBindingHelper<TEntity, TViewModel> UseCaseInsensitiveSearch(bool useCaseInsensitiveSearchOnBothClientAndServerSide = true)
        {
            CaseInsensitiveField = new CaseInsensitive<TEntity>();
            CaseInsensitiveField.BothClientAndServerSide(useCaseInsensitiveSearchOnBothClientAndServerSide);
            return this;
        }

        protected BaseGridCustomBindingHelper<TEntity, TViewModel> UseCaseInsensitiveSearch(Action<ICaseOptions<TEntity>> caseOptions)
        {
            CaseInsensitiveField = new CaseInsensitive<TEntity>();
            caseOptions(CaseInsensitiveField);
            return this;
        }

        protected BaseGridCustomBindingHelper<TEntity, TViewModel> AddAggregateFunctionBase<TAggregateFunction>(Expression<Func<TViewModel, object>> property) where TAggregateFunction : CustomAggregateFunctionBase, new()
        {
            CheckAggregations();

            InheritedClassCustomAggregateCalculatorField = ObjectFactory<TAggregateFunction>.Create();
            CommandField.Aggregates.Create(property.GetPropertyPath(), null, AggregateMethod.Custom);
            return this;
        }

        protected BaseGridCustomBindingHelper<TEntity, TViewModel> AddAggregateFunctionBase(Expression<Func<TViewModel, object>> property, Func<IQueryable<TEntity>, object> entitiesFunction, Func<IQueryable<TViewModel>, object> viewModelFunction = null)
        {
            CheckAggregations();

            ExpressionCustomAggregateFunctionField = entitiesFunction;
            _aggregateViewModelFunctionField = viewModelFunction;
            CommandField.Aggregates.Create(property.GetPropertyPath(), null, AggregateMethod.Custom);
            return this;
        }

        private bool _alreadyAddedAggregation;
        private void CheckAggregations()
        {
            if (_alreadyAddedAggregation)
                throw new NotSupportedException("Only one custom aggregation is supported at a time.");
            _alreadyAddedAggregation = true;
        }

        /// <summary>
        /// Does not calculate the aggregations of each group,
        /// this is good if you only need aggregates in the grids footer area, not to mention that it
        /// greatly reduces round trips in the database.
        /// </summary>
        public BaseGridCustomBindingHelper<TEntity, TViewModel> IgnoreGroupAggregates(bool ignoreGroupAggregates = true)
        {
            IgnoreGroupAggregatesField = ignoreGroupAggregates;
            return this;
        }

        /// <summary>
        /// Projects the ViewModel properties in the database query to return only the columns needed to perform the mapping.
        /// </summary>
        public BaseGridCustomBindingHelper<TEntity, TViewModel> UseProjections(bool useProjections = true)
        {
            UseProjectionsField = useProjections;
            return this;
        }

        public BaseGridCustomBindingHelper<TEntity, TViewModel> UseProjections(Action<ProjectionsOptions<TEntity>> options)
        {
            ProjectionsOptions = new ProjectionsOptionsImpl<TEntity>();
            options.Invoke(ProjectionsOptions);

            UseProjectionsField = true;
            return this;
        }

        public BaseGridCustomBindingHelper<TEntity, TViewModel> AcceptNullValuesWhenFiltering(bool acceptNullValuesWhenFiltering = true)
        {
            AcceptNullValuesWhenFilteringField = acceptNullValuesWhenFiltering;
            return this;
        }

        #endregion
    }
}